package serverlib

import (
	"bufio"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strings"

	"github.com/getkin/kin-openapi/openapi3"

	mycue "github.com/oam-dev/kubevela/pkg/cue"
	"github.com/oam-dev/kubevela/pkg/utils/common"
	"github.com/oam-dev/kubevela/pkg/utils/system"
)

const (
	// OpenAPISchemaDir is the folder name under ~/.vela/capabilities
	OpenAPISchemaDir = "openapi"
	// UsageTag is usage comment annotation
	UsageTag = "+usage="
	// ShortTag is the short alias annotation
	ShortTag = "+short"
)

// OpenAPISchema is the struct for OpenAPI Schema generated by Cue OpenAPI
type OpenAPISchema struct {
	OpenAPI    string     `json:"openapi"`
	Components Components `json:"components"`
}

// Components is the struct filed of OpenAPISchema
type Components struct {
	Schemas Schemas `json:"schemas"`
}

// Schemas is the struct filed of Components
type Schemas struct {
	Parameter map[string]interface{} `json:"parameter"`
}

// GetDefinition is the main function for GetDefinition API
func GetDefinition(name string) ([]byte, error) {
	openAPISchema, err := generateOpenAPISchemaFromCapabilityParameter(name)
	if err != nil {
		return nil, err
	}
	swagger, err := openapi3.NewSwaggerLoader().LoadSwaggerFromData(openAPISchema)
	if err != nil {
		return nil, err
	}
	schemaRef := swagger.Components.Schemas["parameter"]
	schema := schemaRef.Value
	fixOpenAPISchema("", schema)

	parameter, err := schema.MarshalJSON()
	if err != nil {
		return nil, err
	}
	return parameter, nil
}

// generateOpenAPISchemaFromCapabilityParameter returns the parameter of a definition in cue.Value format
func generateOpenAPISchemaFromCapabilityParameter(name string) ([]byte, error) {
	dir, err := system.GetCapabilityDir()
	if err != nil {
		return nil, err
	}

	definitionCueName := fmt.Sprintf("%s.cue", name)
	schemaDir := filepath.Join(dir, OpenAPISchemaDir)
	if err = prepareParameterCue(dir, definitionCueName, schemaDir); err != nil {
		return nil, err
	}

	if err = appendCueReference(filepath.Join(dir, OpenAPISchemaDir, definitionCueName)); err != nil {
		return nil, err
	}

	filename := filepath.FromSlash(definitionCueName)
	return common.GenOpenAPIFromFile(filepath.Join(dir, OpenAPISchemaDir), filename)
}

// prepareParameterCue cuts `parameter` section form definition .cue file
func prepareParameterCue(fileDir, fileName string, targetSchemaDir string) error {
	if _, err := os.Stat(targetSchemaDir); err != nil && os.IsNotExist(err) {
		if err := os.Mkdir(targetSchemaDir, 0750); err != nil {
			return err
		}
	}

	cueFile := filepath.Join(fileDir, fileName)
	f, err := os.Open(filepath.Clean(cueFile))
	if err != nil {
		return err
	}
	//nolint
	defer f.Close()
	schemaFile := filepath.Join(targetSchemaDir, fileName)
	_, err = os.Stat(schemaFile)
	if err == nil {
		if err = os.Truncate(schemaFile, 0); err != nil {
			return err
		}
	}
	targetFile, err := os.OpenFile(filepath.Clean(schemaFile), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
	if err != nil {
		return err
	}
	//nolint
	defer targetFile.Close()

	scanner := bufio.NewScanner(f)
	var withParameterFlag bool
	r := regexp.MustCompile("[[:space:]]*parameter:[[:space:]]*{.*")

	for scanner.Scan() {
		text := scanner.Text()
		if r.MatchString(text) {
			// a variable has to be refined as a definition which starts with "#"
			text = fmt.Sprintf("parameter: #parameter\n#%s", text)
			withParameterFlag = true
		}
		if _, err := targetFile.WriteString(fmt.Sprintf("%s\n", text)); err != nil {
			return err
		}
	}

	if !withParameterFlag {
		return fmt.Errorf("cue file %s doesn't contain section `parmeter`", cueFile)
	}
	return nil
}

// appendCueReference appends `context` filed to parameter .cue file
func appendCueReference(cueFile string) error {
	f, err := os.OpenFile(filepath.Clean(cueFile), os.O_APPEND|os.O_WRONLY, 0600)
	if err != nil {
		return err
	}
	//nolint
	defer f.Close()

	if _, err := f.WriteString(mycue.BaseTemplate); err != nil {
		return err
	}
	return nil
}

// fixOpenAPISchema fixes tainted `description` filed, missing of title `field`.
func fixOpenAPISchema(name string, schema *openapi3.Schema) {
	t := schema.Type
	switch t {
	case "object":
		for k, v := range schema.Properties {
			s := v.Value
			fixOpenAPISchema(k, s)
		}
	case "array":
		fixOpenAPISchema("", schema.Items.Value)
	}
	if name != "" {
		schema.Title = name
	}

	description := schema.Description
	if strings.Contains(description, UsageTag) {
		description = strings.Split(description, UsageTag)[1]
	}
	if strings.Contains(description, ShortTag) {
		description = strings.Split(description, ShortTag)[0]
		description = strings.TrimSpace(description)
	}
	schema.Description = description
}

// getParameterItemName gets the name of a parameter item
func getParameterItemName(previousLine string) (string, error) {
	// parse property name, like from `"cmd": {\n`
	tempPropertyName := strings.Split(previousLine, "\"")
	if len(tempPropertyName) <= 1 {
		return "", fmt.Errorf("could not get property name from: %s", previousLine)
	}
	propertyName := strings.Split(tempPropertyName[1], "\"")[0]
	return propertyName, nil
}

// getParameterFromOpenAPISchema retrieves needs section from OpenAPI schema for front-end requirements
func getParameterFromOpenAPISchema(openAPISchema []byte) ([]byte, error) {
	var schema OpenAPISchema
	var parameterJSON []byte
	var err error
	if err = json.Unmarshal(openAPISchema, &schema); err != nil {
		return nil, err
	}

	parameter := schema.Components.Schemas.Parameter
	if parameterJSON, err = json.Marshal(parameter); err != nil {
		return nil, err
	}
	return parameterJSON, nil
}
